可能有讀者看到 state monad 這個名字時可能會想,Haskell 這樣的語言允許我們擁有 state 這種感覺應該是 mutable 的東西存在嗎?當然不,但是我們還是可以透過 State Monad 來處理這種關於狀態的問題。
那我們可能要先釐清一下什麼是 state ,就我自己的認知我認為 state 就是會根據 「現在」 的值與我 「現在的」 輸入進行運算後產生一個 「新的」 值。那在 Haskell 中我們一般的變數中當然不會有所謂的「現在的」以及「新的」的差異。 所以們可以藉由 State
來幫助我們
newtype State s a = State { runState :: s -> (a,s) }
instance Monad (State s) where
return x = State $ \s -> (x,s)
(State h) >>= f = State $ \s -> let (a, newState) = h s
(State g) = f a
in g newState
State
是用 newtype
將所謂的狀態定義為 s -> (a,s)
,意思是狀態不過是一個 function 然後會傳入狀態 s
最後回傳一個值與新的狀態 (a, s)
這個 turple,運用這種特性我們就能不斷地把 s
也就是狀態保持住並帶到下一次 monadic 操作。
至於 Monad
的實作,return
就是回傳一個預設狀態,這個 function 用來建立一個 State
計算,它不修改狀態,只回傳值 x
。State $ \s -> (x, s)
創建一個 function,該f unction 接受狀態 s
並返回值 x
和相同的狀態 s
。
而 >>=
就比較複雜了點,我們將之前的 h
拿來跟現在的狀態值 s
進行計算
還記得嗎,
State h
不過就是一個 function
藉此來算出一個值 a
以及新的狀態值 newState
這個
a
通常會是State
計算後的結果,像是 stack 在 pop 時會順便回傳原本最上面的那個值
然後將 f
與 a
進行運算來獲得新的 monadic value (State g
、狀態、function)
提醒一下
f
的型別是a → mb
最後再將 g
與 newState
進行運算,來獲得我們最後的結果
總之,>>=
的工作是將先前的 State h
計算的結果 a
再與 f
結合,得到一個新的 State g
,該計算在 newState
上運行。
我們來稍微比較一下有無使用 State
的情況
countNumbers :: State Int Int
countNumbers = do
n <- get
put (n + 1)
return n
countNumbersWithoutState :: Int -> (Int, Int)
countNumbersWithoutState count = (count, count + 1)
let initialCount = 0
let (result1, finalState1) = runState countNumbers initialCount
let (result2, finalState2) = runState countNumbers finalState1
putStrLn $ "State Monad Result 1: " ++ show result1 ++ ", Count 1: " ++ show finalState1
putStrLn $ "State Monad Result 2: " ++ show result2 ++ ", Count 2: " ++ show finalState2
let (result3, state3) = countNumbersWithoutState initialCount
let (result4, state4) = countNumbersWithoutState state3
putStrLn $ "Without State Monad Result 3: " ++ show result3 ++ ", Count 3: " ++ show state3
putStrLn $ "Without State Monad Result 4: " ++ show result4 ++ ", Count 4: " ++ show state4
乍看之下差不多而且 State
的做法感覺更囉唆了點,那這樣子的好處是什麼?目前來看最大的好處是,我們對於上一個狀態值及新的狀態值的管理我們不用自己手動去建立一個 tuple 去處理,以 countNumbersWithoutState
這個 function 來說,我們是手動的方式去強制 tuple 第一個元素一定是上一個狀態值,第二個元素一定是新的狀態值。在只有一個 function 下固然沒什麼差,但只要有更多操作這個 tuple 的 function 出現,這件事情就開始變得有點煩人了。
但如果是使用 State
我們不用自己去煩惱這件事情,因為根據 State
的定義只要我去執行 runState
我就會獲得 s → (a ,s)
,那只要我這個 function 最後是回傳 State
那我就不用再手動去處理 tuple的順序之類的問題,只要直接使用 return
幫我 wrap context 就好。
至於 get
跟 put
就是去對 State
這個 context 的 monadic 操作,一個是取值一個是更新。
那除此之外呢?還記得 Monad
其中一個好處就是可以串聯 monadic 操作嗎?
let (prevState3, state3) = runState (countNumbers >> countNumbers >> countNumbers) state2
print $ "State Monad PrevState 3: " ++ show prevState3 ++ ", Count 3: " ++ show state3
這邊我們就可以簡單的一次執行三次 countNumbers
,但如果是countNumbersWithoutState
我還要去煩惱型別是 Int → (Int , Int)
,這樣子我在每次串接時都要去抽出第二個元素當作下一次計算的參數。
這邊我們是使用
>>
來進行 monadic 操作 ,它與>>=
的差異就是>>
不會將 monadic 操作的結果往後傳而>>=
會 ,但因為我們的countNumbers
其實是沒有參數傳入的他就只是單純執行一個 side effect ,也就是將目前 context 也就是State
進行變更而已,所以就不需要使用>>=
了
今天的程式碼:
https://github.com/toddLiao469469/30days-for-haskell
補充
想當然爾,那些Reader
(我還以為第30天會提)、Writer
、State
monad等我們通常不會自己實作,它們都放在transformers和mtl裡,不僅函式庫的名字有點奇怪,它們的實作也不是newtype而是type synonym,諸如ReaderT
、WriterT
、StateT
之類的,這類XxxT命名方式的data type通常都是Monad transformer。
Monad transformer的作用在於能夠在程式裡方便地組合不同功能的Monad,例如有兩個function定義如下:
data Config = Config
{ databaseSettings :: DatabaseSettings
, ...
}
data User = User ... deriving (Show)
connectDatabase :: DatabaseSettings -> IO Connection
findUserById :: Connection -> Int -> IO (Maybe User)
然後我想寫一個searchUser
的function,能夠結合Reader
(程式的全域設定從這裡讀取)、LoggingT
(搜尋使用者時寫一些log)和Either
(找不到使用者時回傳錯誤訊息)三個Monad的功能,就可以使用Monad transformer來組合它們。
searchUser :: Int -> ReaderT Config (LoggingT (ExceptT String IO)) User
searchUser userId = do
$(logDebug) "Search user"
config <- ask
maybeUser <- liftIO $ do
conn <- connectDatabase (databaseSettings config)
findUserById conn userId
case maybeUser of
Just user -> return user
Nothing -> throwError "No user"
使用起來會需要依據transformer包裹的順序再一層層解開:
main :: IO ()
main = do
let config = Config { databaseSettings = ..., ... }
eitherUser <- runExceptT $ runStdoutLoggingT $ runReaderT (searchUser 1) config
bimapM_ putStrLn print eitherUser
searchUser
的type也還有另外一種寫法,我們稱作tagless final或是mtl style:
searchUser :: (MonadIO m, MonadReader Config m, MonadLogger m, MonadError String m) => Int -> m User
其實就是不要寫死data type,改成type class而已,相當於OOP的依賴介面而非實作
,實際的data type會由function的使用方來決定,function內部的實作只有依賴type class裡提供的function而已,算是最簡單易懂的design pattern。
感謝大大的補充!!
其實主要是因為我還無法理解 mtl 所以就乾脆跳過沒寫了xD
但感覺這樣子才是真正在開發上會用到的形式?